# forward-compat boilerplate
from __future__ import absolute_import
from __future__ import with_statement
__metaclass__ = type

import os
import sys
import time
import os.path as op

import fenton.app


MAXFD = 1024


def get_config(filename, vars=None):
    import ConfigParser
    parser = ConfigParser.ConfigParser(vars)
    if not parser.read(filename):
        raise RuntimeError('Config not found: ' + filename)

    items = parser.items('fenton')
    false = ('off', 'no', 'false')
    return dict((k, False if v and v.lower() in false else v)
                for (k, v) in items)


def _stop_daemon(pidfile):
    if not op.exists(pidfile):
        print 'No PID file exists in %s' % pidfile
        return 0
    pid = _read_pid(pidfile)
    if not pid:
        print 'removing invalid pid file %s' % pidfile
        try:
            os.unlink(pidfile)
        except (OSError, IOError), e:
            print 'Could not delete: %s' % e
            return 2
        return 1
    for _ in range(10):
        if not _read_pid(pidfile):
            break
        import signal
        os.kill(pid, signal.SIGTERM)
        time.sleep(1)
    else:
        print 'failed to kill daemon process %s' % pid
        return 3
    if op.exists(pidfile):
        os.unlink(pidfile)
    return 0


def _write_pid(pidfile):
    import atexit
    pid = os.getpid()
    with open(pidfile, 'w') as f:
        f.write(str(pid))
    atexit.register(_remove_pidfile, pid, pidfile)


def _read_pid(pidfile):
    pid = _read_pidfile(pidfile)
    if pid:
        try:
            os.kill(int(pid), 0)
            return pid
        except OSError, e:
            import errno
            if e.errno == errno.EPERM:
                return pid


def _read_pidfile(pidfile):
    if not op.exists(pidfile):
        return None
    try:
        with open(pidfile) as f:
            return int(f.read().strip())
    except (ValueError, IOError):
        return None


def _remove_pidfile(written_pid, pidfile):
    current_pid = os.getpid()
    if written_pid != current_pid:
        # A forked process must be exiting, not the process that
        # wrote the PID file
        return
    if not op.exists(pidfile):
        return
    with open(pidfile) as f:
        content = f.read().strip()
    try:
        pid_in_file = int(content)
    except ValueError:
        pass
    else:
        if pid_in_file != current_pid:
            msg = 'Unexpected PID %s in file %s (expected %s)' % (pid_in_file,
                                                                  pidfile,
                                                                  current_pid)
            print msg
            return
    try:
        os.unlink(pidfile)
        return
    except OSError, e:
        # Record, but don't give traceback
        print 'Cannot remove PID file: %s' % e
    # well, at least lets not leave the invalid PID around...
    try:
        with open(pidfile, 'w') as f:
            f.write('')
    except OSError, e:
        print 'Stale PID left in file: %s (%e)' % (pidfile, e)
    else:
        print 'Stale PID removed'




class Suite:

    def __init__(self, factory, **vars):
        import optparse
        self.factory = factory
        self.vars = vars
        self.name = factory.__name__
        commands = [ShellCommand,
                    DevServer,
                    RestartDaemon,
                    StartDaemon,
                    StopDaemon]
        self.commands = dict((s.name, s()) for s in commands)
        usage='%prog [global-options] COMMAND [command-options]'
        p = self.parser = optparse.OptionParser(add_help_option=False,
                                                usage=usage)
        p.add_option(
            '-c', '--config',
            action='store',
            metavar='FILE.ini',
            dest='config_file',
            default = factory.config_file
        )
        p.add_option(
            '-h', '--help',
            action='store_true',
            dest='do_help',
            help='Show this help message')
        p.disable_interspersed_args()


    _main = None
    def main(self, f=None):
        'call from __main__'
        if f is not None:
            if self._main is not None:
                raise AssertionError('main already registered')
            self._main = f
            self.install_command_func('main', f)
            sys.argv.append('main')

        from fenton import util
        util.normalize_path()
        args = sys.argv[1:]
        status = 255
        try:
            status = self.dispatch(args)
        except (SystemExit, KeyboardInterrupt), e:
            status = 0
        except:
            import traceback
            traceback.print_exc()
            sys.stdout.flush()
        sys.exit(status)

    def dispatch(self, args):
        options, args = self.parser.parse_args(args)
        if not args or options.do_help:
            args = ['help'] + args

        name = args.pop(0)
        if name == 'help':
            name = args and args.pop(0) or None
            return self.help(name)

        try:
            command = self.commands[name]
        except KeyError:
            print 'ERROR: unknown command \'%s\'' % name
            self.list_commands()
            return 1

        self.config_file = cf = op.realpath(options.config_file)
        self.config = get_config(cf, vars=self.vars)
        return self.run_command(command, args)

    def run_command(self, command, args):
        command.suite = self
        command.parse_args(args)
        return command.run() or 0

    def list_commands(self):
        print '\nCommands:'
        longest = max(map(len, self.commands.keys()))
        for name, command in sorted(self.commands.items()):
            padded = ('%%-%ds' % longest) % name
            print '  %s  %s' % (padded, command.__doc__)

    def help(self, name=None):
        status = 0
        if name is None:
            self.parser.print_help()
            self.list_commands()
        elif name not in self.commands:
            status = 1
        else:
            self.commands[name].parse_args(['-h'])
        return status

    def add_command(self, cmd):
        self.commands[cmd.name] = cmd

    def install_command_func(self, name, func):
        cmd = Command()
        cmd.__doc__ = func.__doc__
        cmd.name = name
        cmd.action = lambda:func(cmd)
        self.commands[cmd.name] = cmd
        return func

    def command(self, arg):
        if isinstance(arg, basestring):
            return lambda f:self.install_command_func(arg, f)
        name = arg.__name__.lower().replace('_', '-')
        return self.install_command_func(name, arg)




class Request(fenton.app.Request):
    def __init__(self, app, cmd):
        import getpass
        import pytz
        from fenton import security
        self.app = app
        self.cmd = cmd
        self.args = cmd.args
        self.user = security.SystemUser(getpass.getuser())
        self.environ = os.environ
        self.tz = pytz.timezone(open('/etc/timezone').read().strip())




class Command:
    __app = __rq = __cx = None
    description = None
    parameters = ()
    factory = property(lambda x:x.suite.factory)
    config = property(lambda x:x.suite.config)
    config_file = property(lambda x:x.suite.config_file)

    def __init__(self):
        import optparse
        self.parser = optparse.OptionParser()
        for args, kw in self.parameters:
            self.parser.add_option(*args, **kw)

    @property
    def app(self):
        if self.__app is None:
            self.__app = self.factory(self, self.config)
        return self.__app

    @property
    def request(self):
        if self.__rq is None:
            self.__rq = Request(self.app, self)
        return self.__rq

    @property
    def context(self):
        if self.__cx is None:
            self.__cx = self.request.new_context()
        return self.__cx

    def run(self):
        self.setup_logging()
        return self.action()

    def parse_args(self, args):
        self.parser.usage = '%%prog [global-options] %s [options]\n%s' % (self.name, self.__doc__)

        if self.description:
            import textwrap
            desc = self.description
            desc = textwrap.dedent(desc)
            self.parser.description = desc
        self.options, self.args = self.parser.parse_args(args)

    def setup_logging(self):
        import logging.config
        logging.config.fileConfig(self.config_file)

    def get_threadpool(self):
        pass




class ShellCommand(Command):
    'Run an interactive python shell'
    name = 'shell'
    def action(self):
        locs = dict(context=self.context,
                    request=self.request,
                    application=self.app)
        from fenton import console
        console.interactive(locs)




class ServerCommand(Command):
    def serve(self):
        from fenton import web, wsgi, logging
        addr = self.config.get('server_addr') or '0.0.0.0'
        port = self.config.get('server_port') or 8000
        stack = web.stack(self.app)
        server = wsgi.CherryPyWSGIServer((addr, int(port)), stack)
        logging.log.info('serving on %s:%s', addr, port)
        server.start()




class DevServer(ServerCommand):
    'Run an auto-reloading webserver'
    environ_key = 'WITH_FENTON_SERVER_RELOADER'
    name = 'dev'
    def run(self):
        from fenton import reloader
        if os.environ.get(self.environ_key):
            self.setup_logging()
            reloader.watch(op.realpath(self.config_file))
            reloader.wait()
            self.serve()
        else:
            return reloader.run(self.environ_key)




class RestartDaemon(Command):
    'Restart daemon'
    name = 'restart'
    def run(self):
        cmd = '%s %s' % (sys.executable, ' '.join(sys.argv))
        os.system(cmd.replace('restart', 'stop'))
        return os.system(cmd.replace('restart', 'start'))




class DaemonFailure(Exception):
    pass




class StartDaemon(ServerCommand):
    'Start a daemon'
    name = 'start'

    parameters = (
        (('--nodaemon',),
             dict(dest='daemon',
                  default=True,
                  action='store_false',
                  help='Do not fork')),

        (('--pidfile',),
             dict(dest='pidfile',
                  metavar='FILE',
                  help='Save PID to file')),

        (('--logfile',),
             dict(dest='logfile',
                  metavar='FILE',
                  help='Save output to the given log file (redirects stdout)')))

    def run(self):

        import locale
        locale.setlocale(locale.LC_ALL, '')
        lang, enc = locale.getlocale()

        pidfile = self.options.pidfile or self.config.get('pidfile')
        if pidfile:
            with open(pidfile, 'a'):
                pass

        logfile = self.options.logfile or self.config.get('logfile')
        logger = None
        if logfile:
            with open(logfile, 'a'):
                pass
            from fenton import util
            maxsize = self.config.get('logfile.maxsize') or '0'
            maxsize = maxsize and util.bytesize(maxsize)
            logger = RotatingLogFile(logfile, 'a', enc)
            if maxsize:
                logger.rotate(maxsize)

        if self.options.daemon:
            try:
                self._fork(pidfile)
            except DaemonFailure, e:
                print str(e)
                return

        if pidfile:
            _write_pid(pidfile)

        if logger:
            sys.stdout = sys.stderr = logger
            self.setup_logging()

        try:
            return self.serve()
        except (SystemExit, KeyboardInterrupt), e:
            if str(e):
                msg = ' ' + str(e)
            else:
                msg = ''
            print 'Exiting%s (-v to see traceback)' % msg

    def get_threadpool(self):
        from paste.httpserver import ThreadPool
        return ThreadPool(5)

    def _fork(self, pidfile):
        pid = _read_pid(pidfile)
        if pid:
            msg = 'Daemon is already running (PID: %s from PID file %s)'
            raise DaemonFailure(msg % (pid, pidfile))

        pid = os.fork()
        if pid:
            # The forked process also has a handle on resources, so we
            # *don't* want proper termination of the process, we just
            # want to exit quick (which os._exit() does)
            os._exit(0)
        # Make this the session leader
        os.setsid()
        # Fork again for good measure!
        pid = os.fork()
        if pid:
            os._exit(0)

        import resource
        maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
        if maxfd == resource.RLIM_INFINITY:
            maxfd = MAXFD
        # close all file descriptors
        for fd in range(0, maxfd):
            try:
                os.close(fd)
            except OSError: # fd wasn't open to begin with (ignored)
                pass

        if (hasattr(os, 'devnull')):
            REDIRECT_TO = os.devnull
        else:
            REDIRECT_TO = '/dev/null'
        os.open(REDIRECT_TO, os.O_RDWR)  # stdin
        # Duplicate stdin to stdout and stderr
        os.dup2(0, 1) # stdout
        os.dup2(0, 2) # stderr




class StopDaemon(Command):
    'Stop a running daemon'
    name = 'stop'
    parameters = (
        (('--pidfile',),
             dict(dest='pidfile',
                  metavar='FILE',
                  help='Save PID to file')),)

    def run(self):
        pidfile = self.config.get('pidfile') or self.options.pidfile
        result = _stop_daemon(pidfile)
        if result:
            print 'Could not stop daemon'
        return result




class RotatingLogFile:

    suffix = '%Y%m%d'

    def __init__(self, filename, mode='w', encoding=None):
        import threading
        self.lock = threading.Lock()
        self.filename = filename
        self.mode = mode
        self.encoding = encoding or 'ASCII'
        self.fd = None

    def open(self):
        if self.fd is None:
            with self.lock:
                if self.fd is None:
                    self.fd = open(self.filename, self.mode)
        return self.fd

    def write(self, text):
        f = self.open()
        if isinstance(text, unicode):
            text = text.encode(self.encoding)
        f.write(text)
        f.flush()

    def writelines(self, text):
        f = self.open()
        f.writelines(text)
        f.flush()

    def flush(self):
        self.open().flush()

    def rotate(self, size):
        if not os.path.exists(self.filename):
            return

        stat = os.stat(self.filename)
        if stat.st_size < size:
            return

        t = time.localtime()
        newfile = self.filename + "." + time.strftime(self.suffix, t)
        if os.path.exists(newfile):
            return

        with self.lock:
            if self.fd is not None:
                self.fd.close()
                self.fd = None
            os.rename(self.filename, newfile)






